package cmd

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"

	"github.com/cenkalti/backoff/v4"
	"github.com/spf13/cobra"
	"github.com/treeverse/lakefs/pkg/api/apigen"
	"github.com/treeverse/lakefs/pkg/api/helpers"
	"github.com/treeverse/lakefs/pkg/logging"
)

const refsRestoreSuccess = `
{{ "All references restored successfully!" | green }}
`

var refsRestoreCmd = &cobra.Command{
	Use:   "refs-restore <repository URI>",
	Short: "Restores refs (branches, commits, tags) from the underlying object store to a bare repository",
	Long: `restores refs (branches, commits, tags) from the underlying object store to a bare repository.

This command is expected to run on a bare repository (i.e. one created with 'lakectl repo create-bare').
Since a bare repo is expected, in case of transient failure, delete the repository and recreate it as bare and retry.`,
	Example: "aws s3 cp s3://bucket/_lakefs/refs_manifest.json - | lakectl refs-restore lakefs://my-bare-repository --manifest -",
	Hidden:  true,
	Args:    cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		repoURI := MustParseRepoURI("repository URI", args[0])
		pollInterval := Must(cmd.Flags().GetDuration("poll-interval"))
		if pollInterval < minimumPollInterval {
			DieFmt("Poll interval must be at least %s", minimumPollInterval)
		}
		timeoutDuration := Must(cmd.Flags().GetDuration("timeout"))

		fmt.Println("Repository:", repoURI)
		manifestFileName := Must(cmd.Flags().GetString("manifest"))
		fp := Must(OpenByPath(manifestFileName))
		defer func() {
			_ = fp.Close()
		}()

		// read and parse the JSON
		data, err := io.ReadAll(fp)
		if err != nil {
			DieErr(err)
		}
		var manifest apigen.RefsRestore
		err = json.Unmarshal(data, &manifest)
		if err != nil {
			DieErr(err)
		}
		// execute the restore operation
		client := getClient()
		ctx := cmd.Context()
		resp, err := client.RestoreSubmitWithResponse(ctx, repoURI.Repository, apigen.RestoreSubmitJSONRequestBody(manifest))
		DieOnErrorOrUnexpectedStatusCode(resp, err, http.StatusAccepted)
		if resp.JSON202 == nil {
			Die("Bad response from server", 1)
		}
		taskID := resp.JSON202.Id
		logging.FromContext(ctx).WithField("task_id", taskID).Debug("Submitted refs restore")

		var bo backoff.BackOff = backoff.NewConstantBackOff(pollInterval)
		bo = backoff.WithContext(bo, ctx)

		restoreStatus, err := backoff.RetryWithData(func() (*apigen.RepositoryRestoreStatus, error) {
			logging.FromContext(ctx).
				WithFields(logging.Fields{"task_id": taskID}).
				Debug("Checking status of refs restore")

			resp, err := client.RestoreStatusWithResponse(ctx, repoURI.Repository, &apigen.RestoreStatusParams{
				TaskId: taskID,
			})
			DieOnErrorOrUnexpectedStatusCode(resp, err, http.StatusOK)
			if resp.JSON200 == nil {
				err := fmt.Errorf("restore status %w: %s", helpers.ErrRequestFailed, resp.Status())
				return nil, backoff.Permanent(err)
			}
			if resp.JSON200.Done {
				return resp.JSON200, nil
			}
			if timeoutDuration >= 0 && time.Since(resp.JSON200.UpdateTime) > timeoutDuration {
				return nil, backoff.Permanent(ErrTaskNotCompleted)
			}
			return nil, ErrTaskNotCompleted
		}, bo)

		switch {
		case err != nil:
			DieErr(err)
		case restoreStatus == nil:
			Die("Refs restore failed: no status returned", 1)
		case restoreStatus.Error != nil:
			DieFmt("Refs restore failed: %s", *restoreStatus.Error)
		}
		Write(refsRestoreSuccess, nil)
	},
}

//nolint:gochecknoinits
func init() {
	rootCmd.AddCommand(refsRestoreCmd)

	refsRestoreCmd.Flags().String("manifest", "", "path to a refs manifest json file (as generated by `refs-dump`). Alternatively, use \"-\" to read from stdin")
	refsRestoreCmd.Flags().Duration("poll-interval", defaultPollInterval, "poll status check interval")
	refsRestoreCmd.Flags().Duration("timeout", defaultPollTimeout, "timeout for polling status checks")
	_ = refsRestoreCmd.MarkFlagRequired("manifest")
}
